package com.jzy.gulimall.product.service.impl;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.map.MapUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jzy.common.utils.PageUtils;
import com.jzy.common.utils.Query;
import com.jzy.gulimall.product.dao.CategoryDao;
import com.jzy.gulimall.product.entity.CategoryEntity;
import com.jzy.gulimall.product.service.CategoryBrandRelationService;
import com.jzy.gulimall.product.service.CategoryService;
import com.jzy.gulimall.product.util.RedisUtil;
import com.jzy.gulimall.product.vo.web.Catelog2Vo;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;


/**
 * 缓存-redis分布式锁-redisson分布式锁-springCacheRedis缓存+sync本地锁
 */
@Service("categoryService")
@Slf4j
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {

    @Resource
    private CategoryBrandRelationService categoryBrandRelationService;

    @Resource
    private RedisUtil redisUtil;

    @Resource
    private RedissonClient redissonClient;

    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        IPage<CategoryEntity> page = this.page(
                new Query<CategoryEntity>().getPage(params),
                new QueryWrapper<CategoryEntity>()
        );

        return new PageUtils(page);
    }

    /**
     * 树形控件 封装列表信息
     */
    @Override
    public List<CategoryEntity> listWithTree() {
        //1.查出所有分类
        List<CategoryEntity> entities = baseMapper.selectList(null);
        //2.组装成父子的树形结构
        //2.1 找到所有的一级分类
        return entities.stream().filter(categoryEntity ->
                categoryEntity.getParentCid() == 0
        ).map(menu -> {
            menu.setChildren(getChildren(menu, entities));
            return menu;
        }).sorted(Comparator.comparingInt(CategoryEntity::getSort)).collect(Collectors.toList());
    }

    //批量删除
    @CacheEvict(value = {"category"}, allEntries = true)
    @Override
    public void removeMenuByIds(List<Long> asList) {
        // TODO: 2020/9/3 检查当前删除的菜单,是否被别的地方引用
        baseMapper.deleteBatchIds(asList);
    }

    /**
     * 查询三级分类路径
     */
    @Override
    public Long[] findCatelogPath(Long catelogId) {
        List<Long> paths = new ArrayList<>();
        List<Long> parentPath = findParentPath(catelogId, paths);
        Collections.reverse(parentPath);
        return parentPath.toArray(new Long[0]);
    }

    /**
     * 级联更新三级分类数据
     * 包括关联表的冗余字段的更新
     * 失效模式 删除redis中的缓存
     * 更新之后删除多个缓存   Caching组合操作 或者CacheEvict的allEntries属性
     */
    //@CacheEvict(value = {"category"},key = "'level1Category'")
    //@CacheEvict(value = {"category"}, allEntries = true)
    @Caching(evict = {
            @CacheEvict(value = {"category"}, key = "'level1Category'"),
            @CacheEvict(value = {"category"}, key = "'catalogJson'")})
    @Override
    @Transactional
    public void updateCascadeById(CategoryEntity category) {
        this.updateById(category);
        categoryBrandRelationService.updateCategory(category.getCatId(), category.getName());
        // TODO: 2020/9/7 更新其他关联表中的冗余信息
    }

    /**
     * 首页 查出所有的一级分类
     * Cacheable 代表该方法的结果需要缓存,如果缓存中有,直接返回.否则将结果写入缓存,然后返回
     * 需要指定放入哪一个名字的缓存 按照类型分 category
     * 优化: 1.指定缓存生产的key 2.指定缓存的存活时间 3.将数据保存为json格式
     * 1.key 2.配置  3.自定义RedisCacheConfiguration
     */
    @Cacheable(value = {"category"}, key = "'level1Category'",sync = true)
    @Override
    public List<CategoryEntity> findLevel1Category() {
        return baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
    }

    /**
     * 优化: 整合cache简化开发流程
     * ,sync = true    加锁(本地锁){读取缓存->如果没有->查询数据库->写入缓存} 解决缓存击穿问题 读锁
     * 默认没有解决   并发修改,更新缓存产生的问题 写锁
     */
    @Cacheable(value = {"category"}, key = "'catalogJson'", sync = true)
    @Override
    public Map<String, List<Catelog2Vo>> getCatalogJson() {
        return dbData();
    }

    /**
     * 首页 封装 二级 三级分类数据
     * index/catalog.json
     * 优化: 将数据库的多次查询变为1次
     * 优化: 引入redis缓存
     * 问题: netty如果没有指定堆外内存 默认使用-Xmx
     * 可以通过-Dio.netty.maxDirectMemory进行设置 治标不治本 没有释放内存
     * 解决: 升级lettuce 使用jedis
     * <p>
     * 优化: 1.空结果缓存: 解决缓存穿透问题
     * 2.过期时间(加随机值):解决缓存雪崩问题
     * 3.加锁:解决缓存击穿问题
     */
    // TODO: 2020/9/13  netty产生堆外内存溢出
    @SuppressWarnings("unchecked")
    public Map<String, List<Catelog2Vo>> getCatalogJsonWithRedisson() {
        Map<String, List<Catelog2Vo>> catalogJson = (Map<String, List<Catelog2Vo>>) redisUtil.get("catalogJson");
        if (MapUtil.isNotEmpty(catalogJson)) {
            return catalogJson;
        }
        //redisUtil.set("catalogJson", catalogJsonFromDb);
        return getCatalogJsonFromDbWithRedisson();
    }

    /**
     * 优化: 使用redisson分布式锁
     * 问题: 缓存数据一致性问题 (1)双写模式 (2)失效模式
     * 优化: 1.读写锁  2.alibaba cancel模拟mysql从服务器向redis发送  3.不管
     */
    private Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisson() {
        //加锁失败自旋    设置过期时间必须和加锁是一个原子操作
        RLock rLock = redissonClient.getLock("catalogLock");

        rLock.lock(30, TimeUnit.SECONDS);
        //加锁成功
        Map<String, List<Catelog2Vo>> catalogJson;
        try {
            catalogJson = getDataFromDb();
        } finally {
            rLock.unlock();
        }
        return catalogJson;
    }

    /**
     * 优化: 使用redis分布式锁
     * 问题1: 在没有释放锁之前,出现异常,导致死锁问题 解决:给锁设置一个过期时间
     * 问题2: 业务超时,锁过期了,如何保证删除的锁是自己的锁 解决:使用一个uuid作为锁的值
     * 问题3: 使用uuid作为锁的值 从redis查看锁,然后删除锁,需要保证原子性
     */
    private Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() {
        //加锁失败自旋    设置过期时间必须和加锁是一个原子操作
        String uuid = UUID.randomUUID().toString(true);
        while (!redisUtil.setnx("lock", uuid, 30)) {
            try {
                TimeUnit.MILLISECONDS.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //加锁成功
        Map<String, List<Catelog2Vo>> catalogJson;
        try {
            catalogJson = getDataFromDb();
        } finally {
            //释放锁   结合lua脚本,保证原子性
            redisUtil.getAndDelLua("lock", uuid);
        }
        /*String lock = (String) redisUtil.get("lock");
        if (uuid.equalsIgnoreCase(lock)) {
            redisUtil.del("lock");
            log.info("redis删除锁");
        }*/
        return catalogJson;
    }

    //从数据库中读取信息,并放入缓存
    @SuppressWarnings("unchecked")
    private Map<String, List<Catelog2Vo>> getDataFromDb() {
        //得到锁之后,先去缓存中判断,如果没有继续查询
        Map<String, List<Catelog2Vo>> catalogJson = (Map<String, List<Catelog2Vo>>) redisUtil.get("catalogJson");
        if (MapUtil.isNotEmpty(catalogJson)) {
            return catalogJson;
        }
        Map<String, List<Catelog2Vo>> map = dbData();
        // TODO: 2020/9/13 需要设置过期时间,否则对数据库进行更新时必须保证数据一致性
        redisUtil.set("catalogJson", map);
        return map;
    }

    private Map<String, List<Catelog2Vo>> dbData() {
        log.info("从数据库读取信息");
        List<CategoryEntity> selectList = baseMapper.selectList(null);
        //1.查出所有一级分类
        //List<CategoryEntity> level1Category = findLevel1Category();
        List<CategoryEntity> level1Category = getParent_cid(selectList, 0);
        //封装结果
        Map<String, List<Catelog2Vo>> map = null;
        if (CollectionUtil.isNotEmpty(level1Category)) {
            //2.封装数据
            map = level1Category.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
                //查出一级分类的二级分类
                List<CategoryEntity> level2Category = getParent_cid(selectList, v.getCatId());
                List<Catelog2Vo> catelog2VoList = null;
                if (CollectionUtil.isNotEmpty(level2Category)) {
                    catelog2VoList = level2Category.stream().map(item2 -> {
                        //二级分类封装结果
                        Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, item2.getCatId().toString(), item2.getName());
                        //查出二级分类的三级分类
                        List<CategoryEntity> level3Category = getParent_cid(selectList, item2.getCatId());
                        if (CollectionUtil.isNotEmpty(level3Category)) {
                            //三级分类封装结果
                            List<Catelog2Vo.Catelog3Vo> catelog3VoList = level3Category.stream().map(item3 -> {
                                return new Catelog2Vo.Catelog3Vo(item2.getCatId().toString(), item3.getCatId().toString(), item3.getName());
                            }).collect(Collectors.toList());
                            catelog2Vo.setCatalog3List(catelog3VoList);
                        }
                        return catelog2Vo;
                    }).collect(Collectors.toList());
                }
                return catelog2VoList;
            }));
        }
        return map;
    }

    /**
     * 首页 封装 二级 三级分类数据
     * index/catalog.json
     * 优化: 将数据库的多次查询变为1次
     * 优化: 加锁:解决缓存击穿问题
     * 优化: 在分布式情况下 本地锁只能锁住当前服务 分布式锁能锁住所有,性能相应下降
     * <p>
     * 问题: 还没有将数据库查询的结果写入缓存,就又有进程进来查询数据库,判断缓存中确实还没有数据
     * 优化: 扩大锁的范围: 将写入缓存操作放在释放锁之前
     */
    public synchronized Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithLocalLock() {
        return getDataFromDb();
    }

    //根据parentCid查询子级分类
    private List<CategoryEntity> getParent_cid(List<CategoryEntity> selectList, long parentCid) {
        return selectList.stream().filter(item -> item.getParentCid() == parentCid).collect(Collectors.toList());
        //return baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", v.getCatId()));
    }

    //递归收集父节点catelogId
    private List<Long> findParentPath(Long catelogId, List<Long> paths) {
        paths.add(catelogId);
        CategoryEntity byId = this.getById(catelogId);
        if (byId.getParentCid() != 0) {
            findParentPath(byId.getParentCid(), paths);
        }
        return paths;
    }

    //递归查找子菜单  root当前菜单  all所有菜单
    private List<CategoryEntity> getChildren(CategoryEntity root, List<CategoryEntity> all) {
        return all.stream().filter(categoryEntity ->
                categoryEntity.getParentCid().equals(root.getCatId())
        ).map(menu -> {
            menu.setChildren(getChildren(menu, all));
            return menu;
        }).sorted(Comparator.comparing(CategoryEntity::getSort, Comparator.nullsFirst(Integer::compareTo))).collect(Collectors.toList());
        /*.sorted((menu1,menu2)->{
            return menu1.getSort() - menu2.getSort();
        })*/
    }

}